<template>
    <view class="drag-scroll-list" :style="{height: props.height}">
        <scroll-view :scroll-y="true" class="scroll-view" :scrollTop="scrollTop" @scroll="handleScroll">
            <view class="container-list">
                <view class="line" :style="{height:`${props.lineHeight}px`, opacity: targetIndex == 0 ? 1 : 0, background: props.lineColor}"></view>
                <template v-for="(item, idx) in props.list" :key="idx">
                    <view class="item real-item bottom-line" :class="{draging: idx == srcIndex}">
                        <slot :item="item" :index="idx"></slot>
                    </view>
                    <view class="line" :style="{height:`${props.lineHeight}px`, opacity: targetIndex == (idx+1) ? 1 : 0, background: props.lineColor}"></view>
                </template>
            </view>
        </scroll-view>
        <view class="ghost" v-if="isDrag && srcIndex >= 0" :style="{
            top: `${displayParentY}px`
        }">
            <view class="item">
                <slot name="ghost" :item="srcItem" :index="srcIndex">
                    <view class="default-ghost"></view>
                </slot>
            </view>
        </view>
    </view>
</template>

<script lang="ts" setup>
import { computed, getCurrentInstance, onMounted, provide, ref, watch } from 'vue';

/**
 * 组件必须设定高度
 **/ 

const props = withDefaults(defineProps<{
    list: any[];
    height: string;
    disabled?: boolean;
    scrollThreshold?: number,
    scrollMinSpeed?: number;
    scrollMaxSpeed?: number;
    lineHeight?: number;//px
    lineColor?: string;
}>(), {
    scrollThreshold: 40,
    scrollMinSpeed: 20,
    scrollMaxSpeed: 150,
    lineHeight: 3,
    lineColor: '#444cf7',
});

const emits = defineEmits<{
    (event: 'change', e: {oldIndex: number, newIndex: number}): void;
}>();

const instance = getCurrentInstance();

const isDrag = ref(false);
const parentY = ref(0);

const scrollTop = ref(0);
let scrollTopCache = 0;
const scrollViewRect = ref<any>();
const listHeight = ref(0);
const itemHeights = ref<number[]>([]);

const displayParentY = computed(()=>{
    return parentY.value - scrollViewRect.value.top;
});

const srcIndex = ref(-1);
const targetIndex = ref(-1);

const srcItem = computed(()=>{
    return props.list[srcIndex.value];
})

onMounted(async ()=>{
    await updateRect();
})

const handleScroll = (e: any)=>{
    scrollTopCache = e.detail.scrollTop;
    // scrollTop.value = e.detail.scrollTop;
}

const handleTouchStart = (e: any, idx: number)=>{
    if(props.disabled) return;

    updateRect();

    isDrag.value = true;
    parentY.value = e.touches[0].clientY;
    srcIndex.value = idx;
}

const handleTouchMove = (e: any)=>{
    if(props.disabled) return;

    parentY.value = e.touches[0].clientY;

    checkAutoScroll();

    targetIndex.value = getTargetIndex();
}

const handleTouchEnd = (e: any)=>{
    if(props.disabled) return;

    isDrag.value = false;

    if(srcIndex.value >= 0 && targetIndex.value >= 0 && srcIndex.value != targetIndex.value){
        let newIndex = targetIndex.value;
        if(srcIndex.value + 1 <= targetIndex.value){
            newIndex = targetIndex.value - 1;
        }

        if(srcIndex.value != newIndex){
            emits('change', {
                oldIndex: srcIndex.value, 
                newIndex: newIndex
            });
        }
    }

    srcIndex.value = -1;
    targetIndex.value = -1;

    stopMove();
}

const updateRect = async ()=>{
    {
        const rect = await getComponentRect('.scroll-view');
        scrollViewRect.value = rect;
    }
    {
        const rect = await getComponentRect('.container-list');
        listHeight.value = rect.height;
    }
    {
        const rects = await getComponentsRect('.real-item');
        itemHeights.value = rects.map(e => e.height);
    }
}

let autoScrollState: 'none'|'top'|'bottom' = 'none';
const checkAutoScroll = ()=>{
    const offset = props.scrollThreshold;
    const minSpeed = props.scrollMinSpeed;
    const maxSpeed = props.scrollMaxSpeed;

    const nearTop = parentY.value - (scrollViewRect.value.top + offset);
    const nearBottom = (scrollViewRect.value.top + scrollViewRect.value.height - offset) - parentY.value;

    if(nearTop < 0){
        autoScrollState = 'top';
        let speed = minSpeed + (-nearTop / offset * (maxSpeed - minSpeed));
        startMove(-speed);
    }
    else if(nearBottom < 0){
        autoScrollState = 'bottom';
        let speed = minSpeed + (-nearBottom / offset * (maxSpeed - minSpeed));
        startMove(speed);
    }else{
        autoScrollState = 'none';
        stopMove();
    }
}

async function getComponentRect (selector: string): Promise<any>{
    return new Promise(next => {
        const query = uni.createSelectorQuery();
        query.in(instance!.proxy);
        query.select(selector).boundingClientRect(res=>{
            next(res);
        });
        query.exec();
    });
}

async function getComponentsRect (selector: string): Promise<any[]>{
    return new Promise(next => {
        const query = uni.createSelectorQuery();
        query.in(instance!.proxy);
        query.selectAll(selector).boundingClientRect(res=>{
            next(res as any[]);
        });
        query.exec();
    });
}

const getTargetIndex = ()=>{
    const localY = toLocalY(parentY.value);

    let index = -1;
    let lastY = 0
    for(let i=0; i<itemHeights.value.length; i++){
        const itemHeight = itemHeights.value[i];

        let startY = lastY;
        if(i==0){
            startY += props.lineHeight;
        }

        let endY = startY + itemHeight + props.lineHeight;

        if(localY > startY && localY < endY){
            if(localY < endY - (itemHeight + props.lineHeight) / 2){
                index = i;
            }else{
                index = i+1;
            }
            break;
        }

        lastY = endY;
    }

    if(index < 0){
        if(localY < 0){
            index = 0;
        }
        if(localY > listHeight.value){
            index = itemHeights.value.length;
        }
    }

    return index;
}

const toLocalY = (y: number) => {
    return y - scrollViewRect.value.top + scrollTopCache;
}

let cancelAnim: (()=>void)|undefined;
let $moveSpeed = 0;
const startMove = (speed: number)=>{
    $moveSpeed = speed;
    if(!cancelAnim){
        cancelAnim = startAnimationFrame((dt: number)=>{
            const max = listHeight.value - scrollViewRect.value.height;
            const temp = scrollTopCache + $moveSpeed * 1 / dt;
            scrollTop.value = Math.max(Math.min(temp, max), 0);
            scrollTopCache = scrollTop.value;
        });
    }
}

const stopMove = ()=>{
    cancelAnim?.();
    cancelAnim = undefined;
}

const startAnimationFrame = (callback: (dt: number) => void) => {
  let dt = Date.now();
  const cancel = setInterval(() => {
    const now = Date.now();
    callback(now - dt);
    dt = now;
  }, 16);
  return ()=>{
    clearInterval(cancel);
  };
};

provide('touchstart', handleTouchStart);
provide('touchmove', handleTouchMove);
provide('touchend', handleTouchEnd);
</script>

<style lang="scss" scoped>
.drag-scroll-list{
    position: relative;

    .scroll-view{
        height: 100%;
        .container-list{
            .line{
                display: block;
                width: 100%;
                height: 0;
                background: #444cf7;
            }            
        }
    }

    .item{
        width: 100%;

        &.draging{
            opacity: 0.3;
            filter: brightness(0.7);
        }
    }

    .ghost{
        z-index: 99999;
        pointer-events: none;
        position: absolute;
        top: 50%;
        width: 100%;
        height: 100%;

        .item{
            .default-ghost{
                height: 84rpx;
                background: #fff;
                border: 2rpx solid #9578ff;
                box-sizing: border-box;
                border-radius: 14rpx;
                background: repeating-linear-gradient( -45deg, #444cf7, #444cf7 5px, #444cf723 5px, #444cf723 20px );
                opacity: 0.5;
            }
        }
    }
}
</style>